package com.starry.module.system.config.oauth2;

import com.starry.core.common.constants.CommonConstant;
import com.starry.core.security.config.SecurityProperties;
import com.starry.core.security.utils.SecurityErrorUtil;
import com.starry.module.system.core.oauth2.authorization.constant.SecurityConstants;
import com.starry.module.system.core.oauth2.authorization.grant.CustomGrantAccessTokenResponseHandler;
import com.starry.module.system.core.oauth2.authorization.grant.CustomGrantAuthenticationProvider;
import com.starry.module.system.core.oauth2.authorization.grant.CustomGrantErrorResponseHandler;
import com.starry.module.system.core.oauth2.authorization.grant.CustomGrantGrantAuthenticationConverter;
import com.starry.module.system.core.oauth2.authorization.handler.LoginFailureHandler;
import com.starry.module.system.core.oauth2.authorization.handler.LoginSuccessHandler;
import com.starry.module.system.core.oauth2.authorization.handler.LoginTargetAuthenticationEntryPoint;
import com.starry.module.system.core.oauth2.authorization.third.ThireClientFactory;
import com.starry.module.system.core.oauth2.service.Oauth2ThirdClientService;
import com.starry.module.system.core.user.service.SysUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

/**
 * 认证配置
 *
 * @Author xiaoke
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class AuthorizationConfig {

    private final AuthConfig authConfig;
    private final SecurityProperties securityProperties;
    private final SysUserService sysUserService;
    private final ThireClientFactory thireClientFactory;

    public AuthorizationConfig(AuthConfig authConfig, SecurityProperties securityProperties, SysUserService sysUserService, ThireClientFactory thireClientFactory) {
        this.authConfig = authConfig;
        this.securityProperties = securityProperties;
        this.sysUserService = sysUserService;
        this.thireClientFactory = thireClientFactory;
    }

    /**
     * 配置端点的过滤器链
     *
     * @param http spring security核心配置类
     * @return 过滤器链
     * @throws Exception 抛出
     */
    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 配置默认的设置，忽略认证端点的csrf校验
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        // 禁用csrf
        http.csrf(AbstractHttpConfigurer::disable);

        // 开启OpenID Connect 1.0协议相关端点（否则资源服务器请求不到）
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());
        // 当未登录时访问认证端点时重定向至login页面
        http.exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginTargetAuthenticationEntryPoint(authConfig),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        ))
                // 处理使用access token访问用户信息端点和客户端注册端点
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));
        // 自定义Grant认证登录转换器
        CustomGrantGrantAuthenticationConverter converter = new CustomGrantGrantAuthenticationConverter();
        // 自定义Grant认证登录认证提供
        CustomGrantAuthenticationProvider provider = new CustomGrantAuthenticationProvider(thireClientFactory);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 让认证服务器元数据中有自定义的认证方式
                .authorizationServerMetadataEndpoint(metadata -> metadata.authorizationServerMetadataCustomizer(customizer -> customizer.grantType(SecurityConstants.GRANT_TYPE_SMS_CODE)))
                .authorizationServerMetadataEndpoint(metadata -> metadata.authorizationServerMetadataCustomizer(customizer -> customizer.grantType(SecurityConstants.GRANT_TYPE_PASSWORD)))
                .authorizationServerMetadataEndpoint(metadata -> metadata.authorizationServerMetadataCustomizer(customizer -> customizer.grantType(SecurityConstants.GRANT_TYPE_THIRD)))
                .authorizationServerMetadataEndpoint(metadata -> metadata.authorizationServerMetadataCustomizer(customizer -> customizer.tokenEndpoint(CommonConstant.INTERNAL_CLIENT_GRANT_TYPE)))
                // 添加自定义grant_type
                .tokenEndpoint(tokenEndpoint -> tokenEndpoint
                        .accessTokenRequestConverter(converter)
                        .accessTokenResponseHandler(new CustomGrantAccessTokenResponseHandler(sysUserService))
                        .errorResponseHandler(new CustomGrantErrorResponseHandler())
                        .authenticationProvider(provider));
        DefaultSecurityFilterChain build = http.build();
        // 从框架中获取provider中所需的bean
        OAuth2TokenGenerator<?> tokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class);
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
        // 以上三个bean在build()方法之后调用是因为调用build方法时框架会尝试获取这些类，
        // 如果获取不到则初始化一个实例放入SharedObject中，所以要在build方法调用之后获取
        // 在通过set方法设置进provider中，但是如果在build方法之后调用authenticationProvider(provider)
        // 框架会提示unsupported_grant_type，因为已经初始化完了，在添加就不会生效了
        provider.setTokenGenerator(tokenGenerator);
        provider.setAuthorizationService(authorizationService);
        provider.setAuthenticationManager(authenticationManager);

        return build;
    }

    /**
     * 配置认证相关的过滤器链
     *
     * @param http spring security核心配置类
     * @return 过滤器链
     * @throws Exception 抛出
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        // 禁用csrf
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests((authorize) -> authorize
                        // 放行静态资源
                        .requestMatchers(securityProperties.getIgnoreAuth()).permitAll().anyRequest().authenticated()
                )
                // 指定登录页面
                .formLogin(formLogin ->
                        formLogin.loginProcessingUrl("/login")
                                // 登录成功和失败改为写回json，不重定向了
                                .successHandler(new LoginSuccessHandler(authConfig.getLoginPageUri()))
                                .failureHandler(new LoginFailureHandler(authConfig.getLoginPageUri()))
                );
        // 添加BearerTokenAuthenticationFilter，将认证服务当做一个资源服务，解析请求头中的token
        http.oauth2ResourceServer((resourceServer) -> resourceServer
                .jwt(Customizer.withDefaults())
                // 自定义鉴权失败响应
                .accessDeniedHandler(SecurityErrorUtil::exceptionHandler)
                .authenticationEntryPoint(SecurityErrorUtil::exceptionHandler)

        );
        // 当未登录时访问认证端点时重定向至login页面
        http.exceptionHandling((exceptions) -> exceptions
                .defaultAuthenticationEntryPointFor(
                        new LoginTargetAuthenticationEntryPoint(authConfig),
                        new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                )
        );

        return http.build();
    }

    /**
     * 添加认证服务器配置，设置jwt签发者、默认端点请求地址等
     *
     * @return AuthorizationServerSettings
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings
                .builder()
                .issuer(authConfig.getIssuerUrl())
                .build();
    }

    /**
     * 配置密码解析器，使用BCrypt的方式对密码进行加密和验证
     *
     * @return BCryptPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
